DataObjects.Net includes consistency validation framework allowing to validate single property values, entire entities and entity graphs.
Each transaction contains its own ValidationContext – class responsible for validation of all entities changed in this transaction. Entity class implements IValidaionAware interface allowing it to be validated by validation context. When some entity is changed, it calls Validate method to inform the appropriate validation context. Validation context can be in one of following states:
You can manage validation context state by opening and closing so-called inconsistency regions. Initially validation context is in consistent state. To change the state, call Session.DisableValidation() method. It turns the context into inconsistent state and returns IDisposable object defining the boundaries of inconsistency region. Validation context returns into consistent state when inconsistent regions is completing (disposing). When it happens, context validates all objects registered for late validation when it was in inconsistent state. If context is already in inconsistent state, this method will return void object that does nothing.
It is recommended to open inconsistent region each time you are about to change a set of closely coupled values. For example, when we are creating some entity and setting a set of property values, we usually validate it once, so we can do it within a single inconsistency region. Our code will look like this:
using (var inconsistencyRegion = session.DisableValidation()) {
Person person = new Person();
person.FirstName = "Mike";
person.LastName = "Groovy";
person.Height = 1.7;
person.BirthDay = new DateTime(1983, 03, 16);
person.IsSubscribedOnNews = true;
person.Email = "mike@groovy.test";
inconsistencyRegion.Complete();
}
In this example the newly created entity will be validated when ‘using’ block is being closed. Method Complete() is called to inform validation context that the block of code is correctly completed, i.e. no exception is thrown inside it. In case this method isn’t called, validation context can not validate entity, because validation exception can hide original exception. So if you catch an exception and continue to work within current transaction, validation will be performed next time you use this context. Note, that you’ll see the same behavior if you simply forget to call Complete() method.
If validation is failed, validation infrastructure will throw AggregateException with list of validation errors found in validated entities:
try {
using (var transactionScope = session.OpenTransaction()) {
using (var inconsistencyRegion = session.DisableValidation()) {
// Change your entities here
inconsistencyRegion.Complete();
}
transactionScope.Complete();
}
}
catch(AggregateException exception) {
Console.WriteLine("Following validation errors were found:");
foreach (var error in exception.GetFlatExceptions())
Console.WriteLine(error.Message);
}
You can also enforce validation of all changed entities inside inconsistency region by calling Session.Validate() method.
Continuous validation can be useful in some cases, but often it is enough to validate all changed entities when transaction is being committed. In such cases you can switch validation mode to OnDemand instead of Continuous. To do this change ValidationMode property of DomainConfiguration instance before domain is built:
domainConfiguration.ValidationMode = ValidationMode.OnDemand;
When this mode is switched on, single inconsistency region is opened for each transaction, so validation context will always be in inconsistent state and entities will be validated on transaction commit only. But you still can validate changed entities with Session.Validate() method any time you want.
To define validation rules you should implement object-level validation logic in OnValidate() method that will be called on entity validation. This method should check entity state and throw an exception if it is invalid.
[HierarchyRoot]
public class Person : Entity
{
// ...
[Field]
public bool IsSubscribedOnNews { get; set;}
[Field]
public string Email { get; set;}
protected override void OnValidate()
{
base.OnValidate();
if (IsSubscribedOnNews && string.IsNullOrEmpty(Email))
throw new Exception("Can't subscribe on news (email is not specified).");
}
}
Another way to define validation rules is to mark entity properties by special attributes – property constraints. Property constraints are special property-level validation aspects that can be applied to any property by marking it with appropriate attribute. Each constraint implements some simple validation rule for property value. For example NotNullConstraint ensures that property value is not null, RegexConstraint ensures that string value matches specified regular expression pattern.
[LengthConstraint(Min = 2, Max = 128)]
[NotNullOrEmptyConstraint]
public string FirstName { get; set;}
[PastConstraint]
public DateTime BirthDay { get; set; }
[EmailConstraint]
public string Email { get; set;}
Each property constraint attribute has two additional properties: Message and Mode.
Message property specifies message for exception that should be thrown if property value is invalid. Message can contain some special variables, that will be replaced automatically: Property value – {value}, property name – {PropertyName} and constraint parameters – {[parameter name]}.
[PastConstraint(Message = "Birth day must be in the past.")]
public DateTime BirthDay { get; set; }
[RangeConstraint(
Min = 0.8,
Max = 2.13,
Message = "Incorrect '{PropertyName}' value: {value}, " +
"it can not be less than {Min} and greater than {Max}.")]
public double Height { get; set;}
If we set height property to 2.5, we’ll get exception with such message: “Incorrect ‘Height’ value: 2.5, it can not be less than 0.8 and greater than 2.13.”
By default property constraints are used when entity is being validated, but it’s also possible to throw exception on setting new value to the property. PropertyConstraintAspect has special property Mode of ConstrainMode type. It has two available values: OnValidate (default value) and OnSetValue.
[PastConstraint(Mode = ConstrainMode.OnSetValue)]
public DateTime BirthDay { get; set; }
DataObjects.Net validation framework includes following predefined constraints:
It is possible to implement your own property constraints inheriting it from abstract class PropertyConstraintAspect.
[Serializable]
public class PhoneNumberConstraint : PropertyConstraintAspect
{
private const string PhoneNumberPattern = "^[2-9]\\d{2}-\\d{3}-\\d{4}$";
[NonSerialized]
private Regex phoneNumberRegex;
public override bool IsSupported(Type valueType)
{
return valueType==typeof(string);
}
public override bool CheckValue(object value)
{
string phoneNumber = (string) value;
return
string.IsNullOrEmpty(phoneNumber) ||
phoneNumberRegex.IsMatch(phoneNumber);
}
protected override string GetDefaultMessage()
{
return "Phone number is incorrect";
}
protected override void Initialize()
{
base.Initialize();
phoneNumberRegex = new Regex(PhoneNumberPattern, RegexOptions.Compiled);
}
}
We create regular expression instance in Initialize() method instead of constructor, we do this since constraints is PostSharp aspects and it is created in compile time also, but we need regex to be created in runtime only.
[PhoneNumberConstraint]
public string Phone { get; set;}